Imports System.ComponentModel
Imports WRDB.Project
Imports DotSpatial.Controls

Public Class clsGIS
    Implements WRDB.Interfaces.clsGIS

    Public Sub ShowGIS(OwnerForm As Form, Mode As WRDB.Interfaces.clsGIS.enumMode, StationList As Generic.List(Of String), Basemap As String) Implements WRDB.Interfaces.clsGIS.ShowGIS
        GISForm = Nothing
        GISForm = New frmGIS(Mode, StationList, Basemap)
        With GISForm
            If OwnerForm Is Nothing Then 'is stand-alone application
                .ShowDialog()
            Else
                .Show(OwnerForm) 'is called from wrdb or graph
            End If
        End With
    End Sub

    ''' <summary>
    ''' Display specified GIS map
    ''' </summary>
    ''' <param name="GisFile">Name of .gis file to load</param>
    ''' <param name="LoadLast">If True, will load previous GIS file</param>
    ''' <remarks></remarks>
    Public Sub ShowGIS(OwnerForm As Form, GisFile As String, LoadLast As Boolean) Implements WRDB.Interfaces.clsGIS.ShowGIS
        GISForm = Nothing
        GISForm = New frmGIS(GisFile, LoadLast)
        With GISForm
            If OwnerForm Is Nothing Then 'is stand-alone application
                .ShowDialog()
            Else
                .Show(OwnerForm) 'is called from wrdb or graph
            End If
        End With
    End Sub

End Class

Public Module modGIS
    ''' <summary>
    ''' The BitBlt function performs a bit-block transfer of the color data corresponding to a rectangle of pixels from the specified source device context into a destination device context.
    ''' </summary>
    ''' <param name="hdcDest">A handle to the destination device context.</param>
    ''' <param name="nXDest">The x-coordinate, in logical units, of the upper-left corner of the destination rectangle.</param>
    ''' <param name="nYDest">The y-coordinate, in logical units, of the upper-left corner of the destination rectangle.</param>
    ''' <param name="nWidth">The width, in logical units, of the source and destination rectangles.</param>
    ''' <param name="nHeight">The height, in logical units, of the source and destination rectangles.</param>
    ''' <param name="hdcSrc">A handle to the source device context.</param>
    ''' <param name="nXSrc">The x-coordinate, in logical units, of the upper-left corner of the source rectangle.</param>
    ''' <param name="nYSrc">The y-coordinate, in logical units, of the upper-left corner of the source rectangle.</param>
    ''' <param name="dwRop">A raster-operation code. These codes define how the color data for the source rectangle is to be combined with the color data for the destination rectangle to achieve the final color. The following list shows some common raster operation codes.</param>
    ''' <returns>If the function succeeds, the return value is nonzero.</returns>
    ''' <remarks></remarks>
    <System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")> _
    Friend Function BitBlt(ByVal hdcDest As IntPtr, ByVal nXDest As Integer, ByVal nYDest As Integer, ByVal nWidth As Integer, ByVal nHeight As Integer, ByVal hdcSrc As IntPtr, ByVal nXSrc As Integer, ByVal nYSrc As Integer, ByVal dwRop As System.Int32) As Boolean
    End Function
    Public DefaultLayerColor(1399) As Drawing.Color
    Friend GISForm As frmGIS
    Public ProjectProjection As DotSpatial.Projections.ProjectionInfo
    'Public Const LatLongProjection As String = "+proj=latlong +ellps=WGS84 +datum=WGS84" '"+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
    Friend CurrentLayer As DotSpatial.Symbology.FeatureLayer  'shared so typeconverter can display list of fields
    Friend WRDBProject As WRDB.Project.clsProject
    Friend ActiveTableName As String
    Friend TempData As String
    Friend WRDBStationTypes As New Generic.List(Of String) 'prior list of station types (so won't have to requery data maybe)
    Friend Const REGAPP As String = "WRDBGIS"

    ''' <summary>Name of WRDB Station layer</summary>
    Friend WRDBStationsLayerName As String = "WRDB Stations"

    ''' <summary>Filename of WRDB Station layer</summary>
    Friend WRDBFileName As String = "WRDBSta"

    ''' <summary>Full filename of WRDB Stations shapefile</summary>
    Friend WRDBStationsFileName As String

    ''' <summary>Name of WRDB Grid layer</summary>
    Friend WRDBGridLayerName As String = "Spatial Grid"

    ''' <summary>Full filename of WRDB Grid shapefile</summary>
    Friend WRDBGridFileName As String = "WRDBGrid"

    ''' <summary>List of stations to display (Stations mode only); string must be tab separated and contain StationID, Name, Type, Lat, Long</summary>
    Friend StationList As New Generic.List(Of String)

    ''' <summary>
    ''' Name of directory for temporary files (does NOT end with backslash)
    ''' </summary>
    Friend TempPath As String = String.Format("{0}\WRDB", My.Computer.FileSystem.SpecialDirectories.Temp)

    Friend Enum enumKey
        NumLayers
        Projection
        GIS_XMin
        GIS_XMax
        GIS_YMin
        GIS_YMax
        GIS_Display
        GIS_PCode
        GIS_Aggregate
        GIS_DateTime
        GIS_AnimSpeed
        GIS_DataFile
        GIS_DataFileRelative
        GIS_DataLayer
        GIS_DataAttribute
        GIS_DataField
        GIS_DataLinkageType
        GIS_DataVectors
        GIS_DataXField
        GIS_DataYField
        GIS_DataArrowLength
        GIS_DataArrowColor
        GIS_DataColor0
        GIS_DataColor1
        GIS_DataColor2
        GIS_DataColor3
        GIS_DataColor4
        GIS_DataInvert
        GIS_DataMinThresholdValue
        GIS_DataMaxThresholdValue
        GIS_NumLegendItems
        GIS_NumCategories
        GIS_ScalingOption
        GIS_OverrideScheme

        ShapeFile
        ShapeFileRelative
        LineColor
        LineWidth
        LineStipple
        Filled
        FillColor
        FillStipple
        FillTransparency
        Visible
        PointColor
        OutlineColor
        PointSize
        PointAngle
        PointShape
        Opacity
        LegendText
        ThemeLegendText
        FilterExpression
        NumCategories
        PatternType
        GradientType
        GradientAngle
        NumGradientColors
        Position
        UseOutline
        AppearsInLegend
        LegendItemVisible
        ForeColor
        BackColor
        HatchStyle
        DashStyle
        MinValue
        MaxValue
        LowColor
        HighColor
        NoDataColor
        Basemap
    End Enum

    ''' <summary>
    ''' Reproject a shapefile to a new projection
    ''' </summary>
    ''' <param name="Map">Map object</param>
    ''' <param name="ShapeFileName">Old shapefile name; if ReplaceOrig is false, this returns the newly reprojected shape file name</param>
    ''' <param name="NewProj">New projection string</param>
    ''' <param name="ReplaceOrig">If true, will replace the original shape file with the new one</param>
    Public Function Reproject(Map As DotSpatial.Controls.Map, ByRef ShapeFileName As String, NewProj As DotSpatial.Projections.ProjectionInfo, ReplaceOrig As Boolean) As Boolean
        Dim fs As DotSpatial.Data.FeatureSet = DotSpatial.Data.FeatureSet.Open(ShapeFileName)
        With fs
            If .DataTable.Rows.Count > 0 AndAlso .Projection.ToProj4String <> NewProj.ToProj4String AndAlso NewProj.IsValid Then
                Dim NewFilename As String = .Filename.Replace(".shp", "-reproject.shp")
                If My.Computer.FileSystem.FileExists(NewFilename) Then DeleteShapeFiles(NewFilename)
                Dim OldProj As DotSpatial.Projections.ProjectionInfo = .Projection
                .Close()
                'if going to reproject and current projection is unknown, assume is lat/long
                If Not fs.Projection.IsValid Then fs.Projection = DotSpatial.Projections.KnownCoordinateSystems.Geographic.World.WGS1984
                fs.Reproject(NewProj)
                If ReplaceOrig Then
                    fs.Save()
                Else
                    fs.SaveAs(NewFilename, True)
                End If
            End If
        End With
        Return True
    End Function

#Region "Map Functions..."

    Friend Function GetFeatureSet(ByVal GISMap As DotSpatial.Controls.Map, ByVal LayerName As String) As DotSpatial.Data.FeatureSet
        'For Each lyr As DotSpatial.Symbology.FeatureLayer In GISMap.Layers
        For Each lyr In GISMap.Layers
            If TypeOf lyr Is DotSpatial.Symbology.FeatureLayer AndAlso lyr.LegendText = LayerName Then
                Return lyr.DataSet
            End If
        Next
        Return Nothing
    End Function

    Friend Function GetFeatureSet(ByVal GISMap As DotSpatial.Controls.Map, ByVal LayerPosition As Integer) As DotSpatial.Data.FeatureSet
        Return GISMap.Layers(LayerPosition).DataSet
    End Function

    Friend Function GetLayerIndex(ByVal GISMap As DotSpatial.Controls.Map, ByVal LayerName As String) As Integer
        'For Each lyr As DotSpatial.Symbology.FeatureLayer In GISMap.GetAllLayers
        For Each lyr In GISMap.GetAllLayers
            If TypeOf (lyr) Is DotSpatial.Symbology.FeatureLayer AndAlso String.Equals(lyr.LegendText, LayerName, StringComparison.InvariantCultureIgnoreCase) Then
                Return GISMap.GetAllLayers.IndexOf(lyr)
            End If
        Next
        Return -1
    End Function

    Friend Function GetLayer(ByVal GISMap As DotSpatial.Controls.Map, ByVal LayerName As String) As DotSpatial.Symbology.Layer
        For Each lyr As DotSpatial.Symbology.Layer In GISMap.Layers
            If lyr.LegendText.ToUpper = LayerName.ToUpper Then Return lyr
        Next
        Return Nothing
    End Function

    Friend Function GetFieldIndex(ByVal dt As DataTable, ByVal FieldName As String)
        For Each c As DataColumn In dt.Columns
            If c.ColumnName.ToUpper.Equals(FieldName.ToUpper) Then Return dt.Columns.IndexOf(c)
        Next
        Return -1
    End Function

    ''' <summary>
    ''' Deletes all files that have same filename as shapefile (e.g., .prj, .idx, .shx, etc.)
    ''' </summary>
    ''' <param name="ShapeFileName">Name of shape file</param>
    Friend Sub DeleteShapeFiles(ShapeFileName As String)
        For i As Int16 = 1 To 5
            Dim ext As String = Choose(i, "shp", "shx", "dbf", "prj", "mwsr")
            Dim fn As String = IO.Path.ChangeExtension(ShapeFileName, ext)
            If My.Computer.FileSystem.FileExists(fn) Then My.Computer.FileSystem.DeleteFile(fn)
        Next
    End Sub

#End Region

    ''' <summary>
    ''' Verify whether file exists in one of two locations; if not, offer to browse to reselect
    ''' </summary>
    ''' <param name="FullFilePath">Full absolute file path to first location</param>
    ''' <param name="BaseDirectory">Full absolute directory path to second location</param>
    ''' <param name="RelativeFilePath">Relative file path to second location</param>
    ''' <returns>Full path of file if found or selected; empty string if cancel at file dialog</returns>
    ''' <remarks>Will first look for file containing full path, then for file at relative location, then in base directory, then will browse</remarks>
    Friend Function FindFile(ByVal FullFilePath As String, Optional ByVal BaseDirectory As String = "", Optional ByVal RelativeFilePath As String = "") As String

        'Static UseSelection As Boolean = False
        Static UseSelection As Boolean = True 'for now, never show dialog and always select in order 

        Dim OrigPath As String = "", RelPath As String = "", LocalPath As String = ""
        Dim NumFound As Integer = 0

        If FullFilePath <> "" And My.Computer.FileSystem.FileExists(FullFilePath) Then OrigPath = FullFilePath : NumFound += 1
        Dim FullFilePath2 As String = ""
        If BaseDirectory <> "" AndAlso My.Computer.FileSystem.DirectoryExists(BaseDirectory) Then
            My.Computer.FileSystem.CurrentDirectory = BaseDirectory
            If RelativeFilePath <> "" Then
                FullFilePath2 = IO.Path.GetFullPath(RelativeFilePath)
                If My.Computer.FileSystem.FileExists(FullFilePath2) Then RelPath = FullFilePath2 : NumFound += 1
            End If
            FullFilePath2 = IO.Path.GetFullPath(BaseDirectory & "\" & IO.Path.GetFileName(FullFilePath))
            If My.Computer.FileSystem.FileExists(FullFilePath2) Then LocalPath = FullFilePath2 : NumFound += 1
            If RelPath = LocalPath Then RelPath = "" : NumFound -= 1
            If OrigPath = LocalPath Then OrigPath = "" : NumFound -= 1
        End If

        Dim path() As String = {OrigPath, RelPath, LocalPath}

        If NumFound = 1 Or UseSelection Then
            If OrigPath <> "" Then Return OrigPath
            If RelPath <> "" Then Return RelPath
            Return LocalPath
        Else
            If NumFound > 1 Then
                With New frmFindFile
                    Dim opt() As RadioButton = {.optOrig, .optRel, .optLocal}
                    Dim txt() As TextBox = {.txtOrig, .txtRel, .txtLocal}
                    For i As Integer = 0 To 2
                        If path(i) <> "" Then
                            opt(i).Enabled = True
                            txt(i).Text = path(i)
                            .ToolTip1.SetToolTip(txt(i), path(i))
                            opt(i).Checked = True 'set last one
                        Else
                            opt(i).Enabled = False
                        End If
                    Next
                    If .ShowDialog = DialogResult.OK Then
                        For i As Integer = 0 To 2
                            If opt(i).Checked Then
                                UseSelection = .chkDontDisplay.Checked
                                Return path(i)
                            End If
                        Next
                        'will browse below
                    Else
                        Return ""
                    End If
                    .Dispose()
                End With
            End If
        End If

        'no files found on any path or user selected browse option

        With New OpenFileDialog
            .FileName = IO.Path.GetFileName(FullFilePath)
            .InitialDirectory = IO.Path.GetDirectoryName(FullFilePath)
            Dim ext As String = IO.Path.GetExtension(FullFilePath)
            .DefaultExt = ext
            .Filter = String.Format("{0} files|*{0}|All files|*.*", ext)
            .FilterIndex = 0
            .Multiselect = False
            .Title = "File not found; please browse to select"
            If .ShowDialog = DialogResult.OK Then
                If .FileName <> "" And My.Computer.FileSystem.FileExists(.FileName) Then Return .FileName
            End If
            Return ""
        End With

    End Function

    Friend Sub ShowHelp()
        Dim FileName As String = WRDB.Project.clsProject.HelpFilePath("WRDB.chm")
        If Not My.Computer.FileSystem.FileExists(FileName) Then
            WarningMsg(StringFormat("Unable to find help file named '{0}'; please restore this file or reinstall application.", FileName.Replace("\", "\\")))
        Else
            Help.ShowHelp(Form.ActiveForm, FileName)
        End If
    End Sub

    Friend Sub ShowHelp(ByVal OwnerForm As Form, ByVal TopicID As String)
        'note: to assign map IDs to help topics, in RoboHelp HTML select Project tab, then Context Sensitive Help, then Aliases, then Map File WRDB.h
        Dim FileName As String = WRDB.Project.clsProject.HelpFilePath("WRDB.chm")
        If Not My.Computer.FileSystem.FileExists(FileName) Then
            WarningMsg(StringFormat("Unable to find help file named '{0}'; please restore this file or reinstall application.", FileName.Replace("\", "\\")))
        Else
            Help.ShowHelp(OwnerForm, FileName, HelpNavigator.TopicId, TopicID)
        End If
    End Sub

    Private ColorCache As New Dictionary(Of Integer, Color)

    Public Function GetKnownColor(Color As Color) As Color
        Dim iArgb As Integer = Color.ToArgb
        If ColorCache.ContainsKey(iArgb) Then Return ColorCache(iArgb)

        Dim nearest_Color As Color = Drawing.Color.Empty
        Dim distance As Double = 500.0

        For Each kcv As KnownColor In [Enum].GetValues(GetType(KnownColor))
            Dim kc As Color = Color.FromKnownColor(kcv)
            Dim dRed As Double = Convert.ToDouble(Color.R)
            Dim dGrn As Double = Convert.ToDouble(Color.G)
            Dim dBlu As Double = Convert.ToDouble(Color.B)
            Dim distRed As Double = Math.Pow(kc.R - dRed, 2.0)
            Dim distGrn As Double = Math.Pow(kc.G - dGrn, 2.0)
            Dim distBlu As Double = Math.Pow(kc.B - dBlu, 2.0)
            Dim dist As Double = Math.Sqrt(distBlu + distGrn + distRed)
            If Not kc.IsSystemColor Then
                If dist = 0.0 Then
                    nearest_Color = kc
                    Exit For
                ElseIf dist < distance Then
                    distance = dist
                    nearest_Color = kc
                End If
            End If
        Next
        ColorCache.Add(iArgb, nearest_Color)
        Return nearest_Color
    End Function

    Friend Enum enumLayerType
        Point
        Line
        Polygon
        Raster
        Image
        TiledImage
        Label
        Group
    End Enum

    Friend Function LayerType(Layer As IMapLayer) As enumLayerType
        If TypeOf Layer Is MapPointLayer Then
            Return enumLayerType.Point
        ElseIf TypeOf Layer Is MapLineLayer Then
            Return enumLayerType.Line
        ElseIf TypeOf Layer Is MapPolygonLayer Then
            Return enumLayerType.Polygon
        ElseIf TypeOf Layer Is MapRasterLayer Then
            Return enumLayerType.Raster
        ElseIf TypeOf Layer Is MapImageLayer Then
            Return enumLayerType.Image
        ElseIf TypeOf Layer Is MapTiledImageLayer Then
            Return enumLayerType.TiledImage
        ElseIf TypeOf Layer Is MapLabelLayer Then
            Return enumLayerType.Label
        ElseIf TypeOf Layer Is MapGroup Then
            Return enumLayerType.Group
        Else
            Throw New InvalidCastException
        End If
    End Function

    Public Sub Main()
        With New frmGIS
            .ShowDialog()
            .Dispose()
        End With
    End Sub

    Private _HandCursor As Cursor = Nothing

    Public ReadOnly Property HandCursor As Cursor
        Get
            If _HandCursor Is Nothing Then
                Dim ms As New System.IO.MemoryStream(My.Resources.cursorHand)
                _HandCursor = New Cursor(ms)
            End If
            Return _HandCursor
        End Get
    End Property

End Module


''' <summary>
''' This was tested for display of available field names in selected layer, but decided to go another direction
''' </summary>
''' <remarks></remarks>
Public Class FieldListConverter
    Inherits TypeConverter

    Public Overrides Function GetStandardValuesSupported(ByVal context As System.ComponentModel.ITypeDescriptorContext) As Boolean
        Return True
    End Function

    Public Overrides Function GetStandardValuesExclusive(ByVal context As System.ComponentModel.ITypeDescriptorContext) As Boolean
        Return True
    End Function

    Public Overrides Function GetStandardValues(ByVal context As System.ComponentModel.ITypeDescriptorContext) As System.ComponentModel.TypeConverter.StandardValuesCollection
        Dim lstFields As New Generic.List(Of String)

        lstFields.Add("(None)")
        For i As Integer = 0 To CurrentLayer.DataSet.DataTable.Columns.Count - 1
            lstFields.Add(CurrentLayer.DataSet.DataTable.Columns(i).ColumnName)
        Next
        lstFields.Sort()
        Return New StandardValuesCollection(lstFields)
    End Function
End Class


''' <summary>
''' This class combines the functionality of MapFunctionSelect and MapFunctionPan so that you can do a 'transparent' zoom/pan while selecting
''' </summary>
''' <remarks></remarks>
Friend Class MapFunctionSelectPan
    Inherits MapFunctionSelect

    Private _dragStart As System.Drawing.Point
    Private _isDragging As Boolean
    Private _preventDrag As Boolean
    Private _source As Rectangle

    Private _client As Rectangle
    Private _direction As Integer = 1
    Private _mapFrame As IMapFrame
    Private _sensitivity As Double
    Private _timerInterval As Integer
    Private WithEvents _zoomTimer As Timer

    Sub New(inMap As IMap)
        MyBase.New(inMap)
        MyBase.Map.Cursor = Cursors.Hand
        inMap.FunctionMode = FunctionMode.None
        Configure()
    End Sub

    Private Sub Configure()
        YieldStyle = YieldStyles.Scroll
        _timerInterval = 100
        _zoomTimer = New Timer() With {.Interval = _timerInterval}
        _client = Rectangle.Empty
        Sensitivity = 0.5
        Name = "ScrollZoom"
    End Sub


    Private Sub _zoomTimer_Tick(sender As Object, e As EventArgs) Handles _zoomTimer.Tick
        _zoomTimer.Stop()
        If _mapFrame Is Nothing Then
            Return
        End If
        _client = Rectangle.Empty
        _mapFrame.ResetExtents()
        Map.IsBusy = False
    End Sub

    ''' <summary>
    ''' Gets or sets the wheel zoom sensitivity.  Increasing makes it more sensitive.  Maximum is 0.5, Minimum is 0.01
    ''' </summary>
    Public Property Sensitivity() As Double
        Get
            Return 1.0 / _sensitivity
        End Get
        Set(value As Double)
            If value > 0.5 Then
                value = 0.5
            ElseIf value < 0.01 Then
                value = 0.01
            End If
            _sensitivity = 1.0 / value
        End Set
    End Property

    Protected Overrides Sub OnMouseWheel(e As DotSpatial.Controls.GeoMouseArgs)
        _zoomTimer.Stop()
        ' if the timer was already started, stop it.
        Dim r As Rectangle = e.Map.MapFrame.View

        ' For multiple zoom steps before redrawing, we actually
        ' want the x coordinate relative to the screen, not
        ' the x coordinate relative to the previously modified view.
        If _client = Rectangle.Empty Then
            _client = r
        End If
        Dim cw As Integer = _client.Width
        Dim ch As Integer = _client.Height

        Dim w As Double = r.Width
        Dim h As Double = r.Height

        If _direction * e.Delta > 0 Then
            Dim inFactor As Double = 2.0 * _sensitivity
            r.Inflate(Convert.ToInt32(-w / inFactor), Convert.ToInt32(-h / inFactor))
            ' try to keep the mouse cursor in the same geographic position
            r.X += Convert.ToInt32((e.X * w / (_sensitivity * cw)) - w / inFactor)
            r.Y += Convert.ToInt32((e.Y * h / (_sensitivity * ch)) - h / inFactor)
        Else
            Dim outFactor As Double = 0.5 * _sensitivity
            r.Inflate(Convert.ToInt32(w / _sensitivity), Convert.ToInt32(h / _sensitivity))
            r.X += Convert.ToInt32(w / _sensitivity - (e.X * w / (outFactor * cw)))
            r.Y += Convert.ToInt32(h / _sensitivity - (e.Y * h / (outFactor * ch)))
        End If

        e.Map.MapFrame.View = r
        e.Map.Invalidate()
        _zoomTimer.Start()
        _mapFrame = e.Map.MapFrame
        Map.IsBusy = True
        MyBase.OnMouseWheel(e)
    End Sub

    Protected Overrides Sub OnMouseDown(e As DotSpatial.Controls.GeoMouseArgs)
        MyBase.OnMouseDown(e)
        If e.Button = MouseButtons.Middle AndAlso _preventDrag = False Then
            MyBase.Map.Cursor = HandCursor
            _isDragging = True
            _dragStart = e.Location
            _source = e.Map.MapFrame.View
            Map.IsBusy = True
        End If
    End Sub

    Protected Overrides Sub OnMouseUp(e As DotSpatial.Controls.GeoMouseArgs)
        MyBase.OnMouseUp(e)
        If e.Button = MouseButtons.Middle And _isDragging Then
            _isDragging = False

            _preventDrag = True
            e.Map.MapFrame.ResetExtents()
            _preventDrag = False
            Map.IsBusy = False

            MyBase.Map.Cursor = Cursors.Hand 'resume selection cursor
        End If
    End Sub

    Protected Overrides Sub OnMouseMove(e As DotSpatial.Controls.GeoMouseArgs)
        If _isDragging Then
            Dim diff As New Point() With {.X = _dragStart.X - e.X, .Y = _dragStart.Y - e.Y}
            e.Map.MapFrame.View = New Rectangle(_source.X + diff.X, _source.Y + diff.Y, _source.Width, _source.Height)
            Map.Invalidate()
        End If
        MyBase.OnMouseMove(e)
    End Sub

End Class
